package com.bocai.duobao.service;

import com.alibaba.fastjson.TypeReference;
import com.bocai.CacheKey;
import com.bocai.Constants;
import com.bocai.PageResult;
import com.bocai.config.RedisAdapter;
import com.bocai.duobao.mapper.DuobaoMapper;
import com.bocai.duobao.model.BetNumInfo;
import com.bocai.duobao.model.OpeningData;
import com.bocai.duobao.model.SimpleBetRecord;
import com.bocai.duobao.model.entity.DbBetDetail;
import com.bocai.duobao.model.entity.DbGame;
import com.bocai.duobao.model.entity.DbOpenRecord;
import com.bocai.duobao.model.enums.SpendType;
import com.bocai.duobao.model.enums.WinStatus;
import com.bocai.duobao.model.param.GameDetail;
import com.bocai.duobao.model.param.GameList;
import com.bocai.duobao.model.param.OpenRecord;
import com.bocai.user.mapper.UserMapper;
import com.bocai.user.model.CommissionRecord;
import com.bocai.user.model.PaybackRecord;
import com.bocai.user.model.User;
import com.bocai.user.model.UserAsset;
import com.bocai.user.model.entity.BetDetail;
import com.bocai.user.service.UserService;
import com.bocai.utils.CommonUtils;
import com.bocai.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author create by VIC.Huang
 * @date 2018/1/16
 */
@Service
public class DuobaoService {
    @Autowired
    private DuobaoMapper duobaoMapper;
    @Autowired
    private RedisAdapter redisAdapter;
    @Autowired
    private UserService userService;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RestTemplate restTemplate;

    public List<GameList> getDuobaoList() {
        List<DbGame> gameList;
        if(redisAdapter.exists(CacheKey.GAME_LIST)) {
            gameList = redisAdapter.get(CacheKey.GAME_LIST, new TypeReference<List<DbGame>>(){});
        }else {
            gameList = duobaoMapper.selectGames();
            redisAdapter.setex(CacheKey.GAME_LIST, 3600, gameList);
        }
        if (null != gameList && 0 < gameList.size()) {
            List<GameList> resultList = new ArrayList<>(gameList.size());
            int leftSecond = getLeftSecond();
            for (DbGame game : gameList) {
                GameList temp = new GameList();
                BeanUtils.copyProperties(game, temp);
                temp.setLeftSecond(leftSecond);
                temp.setGameId(game.getId());
                resultList.add(temp);
            }
            return resultList;
        }
        return Collections.emptyList();
    }

    /**
     * 获取开奖时间倒计时数（秒）
     * 10：00～22：00 十分钟开奖一次 ；   22：00～2：00 五分钟一次
     *
     * @return
     */
    public int getLeftSecond() {
        int leftSecond;
        LocalTime nowTime = LocalTime.now();
        if (nowTime.isAfter(LocalTime.of(9, 50)) && nowTime.isBefore(LocalTime.of(22, 0))) {
            // 提前十秒结束，下同
            leftSecond = (590 - ((nowTime.getMinute() * 60 + nowTime.getSecond()) % 600));
        } else if (nowTime.isAfter(LocalTime.of(22, 0)) || nowTime.isBefore(LocalTime.of(2, 0))) {
            leftSecond = (290 - ((nowTime.getMinute() * 60 + nowTime.getSecond()) % 300));
        } else {
            leftSecond = 3599;
        }
        return leftSecond < 0 ? -1 : leftSecond;
    }

    //加缓存1s
    public GameDetail getDuobaoDetail(Long gameId, String token) {
        String cacheKey = CacheKey.GAME_DETAIL + gameId;
        DbGame game;
        if(redisAdapter.exists(cacheKey)) {
            game = redisAdapter.get(cacheKey, new TypeReference<DbGame>(){});
        }else {
            game = duobaoMapper.selectGameById(gameId);
            redisAdapter.setex(cacheKey, 3600, game);
        }
        if (null != game) {
            GameDetail detail = new GameDetail();
            BeanUtils.copyProperties(game, detail);
            detail.setGameId(game.getId());
            detail.setLeftSecond(getLeftSecond());
            detail.setIssueNo(getIssueNo());

            if(StringUtils.isNotBlank(token) && redisAdapter.exists(CacheKey.TOKEN_PREFIX + token)) {
                UserAsset userAsset = userService.getUserAsset(Long.parseLong(redisAdapter.get(CacheKey.TOKEN_PREFIX + token)));
                if(null != userAsset) {
                    detail.setLeftMoney(userAsset.getAsset());
                }else {
                    detail.setLeftMoney(BigDecimal.ZERO);
                }
            }else {
                detail.setLeftMoney(BigDecimal.ONE.negate());
            }

            return detail;
        }
        return null;
    }

    private static String issueNo = null;
    private static final String SSC_URL = "http://a.apiplus.net/newly.do?token=9e7c7683ae211a3c&code=cqssc&format=json";
    private static Lock lock = new ReentrantLock();    // 初始化issueNo加锁

    public String getIssueNo() {
        while(null == issueNo) {
            changeIssueNo();
        }
        return issueNo;
    }

    @Scheduled(cron = "0 0/10 9-21 * * ?")
    @Scheduled(cron = "0 0/5 22-23 * * ?")
    @Scheduled(cron = "0 0/5 0-1 * * ?")
    protected void changeIssueNo() {
        if (lock.tryLock()) {
            try {
                OpeningData openingData = getLatestOpeningDataFromApi();
                LocalTime nowTime = LocalTime.now();
                LogManager.getLogger().info("******* Time:{} ******* OpeningData:{}", nowTime, openingData);
                if (nowTime.isAfter(LocalTime.of(10, 0)) && nowTime.isBefore(LocalTime.of(22, 0))) {
                    OpeningData.Datas datas = openingData.getData().get(0);
                    LocalTime openTime = Instant.ofEpochSecond(datas.getOpentimestamp()).atZone(ZoneId.systemDefault()).toLocalTime();
                    if (nowTime.getMinute() / 10 == openTime.getMinute() / 10) {
                        issueNo = String.valueOf(Long.parseLong(datas.getExpect()) + 1);
                    } else {
                        issueNo = String.valueOf(Long.parseLong(datas.getExpect()) + 2);
                    }
                } else if(nowTime.isAfter(LocalTime.of(0, 0)) && nowTime.isBefore(LocalTime.of(0, 5, 0))) {
                    issueNo = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "001";
                } else if(nowTime.isAfter(LocalTime.of(0, 5)) && nowTime.isBefore(LocalTime.of(0, 10, 0))) {
                    issueNo = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "002";
                } else if((nowTime.isAfter(LocalTime.of(22, 0)) && nowTime.isBefore(LocalTime.of(23, 59))) ||
                        (nowTime.isAfter(LocalTime.of(0, 10)) && nowTime.isBefore(LocalTime.of(2, 0)))) {
                    OpeningData.Datas datas = openingData.getData().get(0);
                    LocalTime openTime = Instant.ofEpochSecond(datas.getOpentimestamp()).atZone(ZoneId.systemDefault()).toLocalTime();
                    if (nowTime.getMinute() / 5 == openTime.getMinute() / 5) {
                        issueNo = String.valueOf(Long.parseLong(datas.getExpect()) + 1);
                    } else {
                        issueNo = String.valueOf(Long.parseLong(datas.getExpect()) + 2);
                    }
                } else {
                    issueNo = "000";
                }
            } catch (Exception e) {
                LogManager.getLogger(DuobaoService.class).error("Failed to get IssueNo!", e);
                changeIssueNo();
            } finally {
                lock.unlock();
            }

        }
    }

    /**
     * 请求开奖结果接口
     */
    private OpeningData getLatestOpeningDataFromApi() throws IOException {
        // data : [{"expect":"20170705095","opencode":"5,0,3,8,3","opentime":"2017-07-05 21:50:40","opentimestamp":1499262640},...]
        return JsonUtils.fromJson(restTemplate.getForObject(SSC_URL, String.class), OpeningData.class);
    }

    /**
     * 下注
     *
     * @param betDetail
     */
    @Transactional(rollbackFor = Exception.class)
    public void bet(DbBetDetail betDetail, String token) throws Exception {
        LogManager.getLogger(DuobaoService.class).info(betDetail);
        try {
            LocalTime nowTime = LocalTime.now();
            if ((nowTime.isAfter(LocalTime.of(2, 0)) && nowTime.isBefore(LocalTime.of(10, 0)))) {
                throw new RuntimeException("封盘不可下注！");
            }
            if (-1 == getLeftSecond()) {
                throw new RuntimeException("封盘不可下注！");
            }
            // 下注期数有效性验证
            if (!getIssueNo().equals(betDetail.getIssueNo())) {
                throw new RuntimeException("下注期数有误！");
            }

            validBetNum(betDetail.getBetNum());

            // 根据gameId与betNum获取单注下注金额 及 限制下注数
            BetNumInfo betNumInfo = duobaoMapper.selectBetNumInfo(betDetail.getDbGameId(), betDetail.getBetNum());
            if (null == betNumInfo) {
                throw new RuntimeException("【异常】无法下注!");
            }

            // 是否超出下注数限额
            if (betDetail.getAmount() > betNumInfo.getLimitAmount()) {
                throw new RuntimeException("超出下注数限额【" + betNumInfo.getLimitAmount() + "】！");
            }

            // 验证用户账户余额是否支持本次下注
            Long userId = Long.parseLong(redisAdapter.get(CacheKey.TOKEN_PREFIX + token));
            BigDecimal totalBetPrice = betNumInfo.getPerPrice().multiply(new BigDecimal(betDetail.getAmount()));
            if(!userService.checkEnoughBalance(userId, totalBetPrice)) {
                throw new RuntimeException("账户余额不足!");
            }

            betDetail.setUserId(userId);
            betDetail.setPerPrice(betNumInfo.getPerPrice()).setPerWinPrice(betNumInfo.getPerWinPrice());
            betDetail.setDbGameListImgPath(betNumInfo.getDbGameListImgPath());
            betDetail.setOrderNo(CommonUtils.genOrderNum(betDetail.getIssueNo()));
            betDetail.setDbGameTitle(betNumInfo.getDbGameTitle());
            // 进行下注操作
            if (duobaoMapper.insertBetDetail(betDetail) == 0) {
                throw new RuntimeException("【异常】下注失败!");
            }

            // 更新用户余额
            if(!userService.deductBalance(userId, totalBetPrice, SpendType.BET.getCode())) {
                throw new RuntimeException("账户扣费失败！");
            }
            // 为推荐人添加佣金记录 及 下注人反水
            User user = userService.getUserById(userId);
            if(null != user.getFromuid() && 0L !=  user.getFromuid()) {
                addCommission(user.getFromuid(), userId, user.getNickName(),
                        betDetail.getAmount(), betDetail.getDbGameId(), betDetail.getBetNum());
            }
            addPayback(userId, betDetail.getAmount(), betDetail.getDbGameId(), betDetail.getBetNum());
        } catch (Exception e) {
            LogManager.getLogger(DuobaoService.class).error("ERROR => ", e);
            throw new RuntimeException(e.getMessage());
        }

    }

    @Transactional(rollbackFor = Exception.class)
    protected void addCommission(Long userId, Long fromUserId, String fromUserName,
                                 int betAmount, Long gameId, String betNum) {
        try {
            BigDecimal perCommission = BigDecimal.ZERO;
            if(Constants.TWO.containsKey(betNum)) {
                perCommission = new BigDecimal(Constants.PER_COMMISSION.get(gameId + "two"));
            }else if(Constants.FOUR.containsKey(betNum)) {
                perCommission = new BigDecimal(Constants.PER_COMMISSION.get(gameId + "four"));
            }else if(Constants.TEN.containsKey(betNum)) {
                perCommission = new BigDecimal(Constants.PER_COMMISSION.get(gameId + "ten"));
            }
            BigDecimal commission = perCommission.multiply(new BigDecimal(betAmount)).setScale(2, BigDecimal.ROUND_HALF_UP);
            CommissionRecord cr = new CommissionRecord();
            cr.setMoney(commission).setUserId(userId).setFromUserId(fromUserId).setFromUserName(fromUserName);
            userMapper.insertCommissionRecord(cr);
            userMapper.updateUserAsset(userId, commission, BigDecimal.ZERO);
        } catch (Exception e) {
            LogManager.getLogger(DuobaoService.class).error("添加佣金失败！", e);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    protected void addPayback(Long userId, int betAmount, Long gameId, String betNum) {
        try {
            BigDecimal perPayback = BigDecimal.ZERO;
            if(Constants.TWO.containsKey(betNum)) {
                perPayback = new BigDecimal(Constants.PER_PAY_BACK.get(gameId + "two"));
            }else if(Constants.FOUR.containsKey(betNum)) {
                perPayback = new BigDecimal(Constants.PER_PAY_BACK.get(gameId + "four"));
            }else if(Constants.TEN.containsKey(betNum)) {
                perPayback = new BigDecimal(Constants.PER_PAY_BACK.get(gameId + "ten"));
            }
            BigDecimal payback = perPayback.multiply(new BigDecimal(betAmount)).setScale(2, BigDecimal.ROUND_HALF_UP);
            PaybackRecord pr = new PaybackRecord();
            pr.setUserId(userId).setMoney(payback);
            userMapper.insertPaybackRecord(pr);
            userMapper.updateUserAsset(userId, payback, BigDecimal.ZERO);
        } catch (Exception e) {
            LogManager.getLogger(DuobaoService.class).error("添加反水失败！", e);
        }
    }

    /**
     * 检测下注号码是否正确
     *
     * @param betNum
     */
    private void validBetNum(String betNum) {
        if (!Constants.TWO.keySet().contains(betNum) && !Constants.FOUR.keySet().contains(betNum) && !Constants.TEN.keySet().contains(betNum)) {
            throw new RuntimeException("下注号码值有误！");
        }
    }

    public PageResult<OpenRecord> getOpenRecords(int page) {
        if(redisAdapter.exists(CacheKey.OPEN_RECORDS)) {
            return redisAdapter.get(CacheKey.OPEN_RECORDS, new TypeReference<PageResult<OpenRecord>>(){});
        }else {
            PageResult<OpenRecord> result = new PageResult<>(page,
                    duobaoMapper.countOpenRecords(),
                    duobaoMapper.selectOpenRecords((page-1)*Constants.PAGE_SIZE, Constants.PAGE_SIZE));

            redisAdapter.setex(CacheKey.OPEN_RECORDS + page, 60, result);
            return result;
        }
    }

    public PageResult<SimpleBetRecord> getLatestBetRecords(Long gameId, Integer page) {
        String cacheKey = CacheKey.LATEST_BET_RECORDS + gameId + page;
        if(redisAdapter.exists(cacheKey)) {
            return redisAdapter.get(cacheKey, new TypeReference<PageResult<SimpleBetRecord>>(){});
        }else {
            if(0 == gameId) {
                gameId = null;
            }
            PageResult<SimpleBetRecord> result = new PageResult<>(
                    page,
                    userMapper.countLatestBetRecords(gameId, null),
                    userMapper.selectLatestBetRecords(gameId, null, (page-1)*Constants.PAGE_SIZE, Constants.PAGE_SIZE)
            );
            redisAdapter.setex(cacheKey, 60, result);
            return result;
        }
    }

    public PageResult<SimpleBetRecord> getLatestWinRecords(Integer page) {
        String cacheKey = CacheKey.LATEST_WIN_RECORDS + page;
        if(redisAdapter.exists(cacheKey)) {
            return redisAdapter.get(cacheKey, new TypeReference<PageResult<SimpleBetRecord>>(){});
        }else {
            PageResult<SimpleBetRecord> result = new PageResult<>(
                    page,
                    userMapper.countLatestBetRecords(null, WinStatus.WIN.getCode()),
                    userMapper.selectLatestBetRecords(null, WinStatus.WIN.getCode(), (page-1)*Constants.PAGE_SIZE, Constants.PAGE_SIZE)
            );
            redisAdapter.setex(cacheKey, 60, result);
            return result;
        }
    }

    /**
     * 开奖结果处理
     */
    @Scheduled(cron = "20 1/10 10-21 * * ?")
    @Scheduled(cron = "20 1/5 22-23 * * ?")
    @Scheduled(cron = "20 1/5 0-2 * * ?")
    @Scheduled(cron = "55 1/10 10-21 * * ?")
    @Scheduled(cron = "55 1/5 22-23 * * ?")
    @Scheduled(cron = "55 1/5 0-2 * * ?")
    public void handleOpening() {
        try {
            OpeningData openData = JsonUtils.fromJson(restTemplate.getForObject(SSC_URL, String.class), OpeningData.class);
            for (int i = openData.getRows() - 1; i >= 0; i--) {
                OpeningData.Datas data = openData.getData().get(i);
                String issueNo = data.getExpect();        // 期数
                String rawOpenCode = data.getOpencode();    // 开奖号码
                String openTime = data.getOpentime();    // 开奖时间

                if (duobaoMapper.countDbOpenRecordByIssueNo(issueNo) == 0) {
//                    int lastCode = Integer.parseInt(rawOpenCode.substring(rawOpenCode.length() - 1));
                    List<String> codeList = Arrays.asList(rawOpenCode.split(","));
                    DbOpenRecord openRecord = new DbOpenRecord();
                    openRecord.setIssueNo(issueNo)
                            .setOpenCode(rawOpenCode)
                            .setOpenResult(genPrizeCode(codeList))
                            .setOpenResultDesc(genPrizeDtl(codeList))
                            .setOpenTime(openTime);
                    duobaoMapper.insertDbOpenRecord(openRecord);
                }
            }
            handleWinningRecord();
        } catch (Exception e) {
            LogManager.getLogger(DuobaoService.class).error("Failed to grabShishicaiPrize! ERROR ==============>", e);
        }
    }

    private String genPrizeDtl(List<String> codeList) {
        StringBuilder sb = new StringBuilder();
        int lastSecondCode = Integer.parseInt(codeList.get(3));     // 倒数第二个数字
        int lastCode = Integer.parseInt(codeList.get(4));           // 倒数第一个数字
        // 双人玩法
        // -- 大/小
        if(lastCode <= 4) {
            sb.append("小");
        }else {
            sb.append("大");
        }
        sb.append(" | ");
        // -- 单/双
        if(lastCode % 2 == 0) {
            sb.append("双");
        }else {
            sb.append("单");
        }
        sb.append(" | ");
        // 四人玩法
        if(lastSecondCode <= 4) {   // 小
            if(lastCode % 2 == 0) {
                sb.append("小双");
            }else {
                sb.append("小单");
            }
        }else {     // 大
            if(lastCode % 2 == 0) {
                sb.append("大双");
            }else {
                sb.append("大单");
            }
        }
        sb.append(" | ");
        // 十人
        sb.append(lastCode);
        return sb.toString();
    }

    /**
     * 生成开奖结果编码（如  max,even,max_even,8）
     * @return
     */
    private String genPrizeCode(List<String> codeList) {
        StringBuilder sb = new StringBuilder();
        int lastSecondCode = Integer.parseInt(codeList.get(3));     // 倒数第二个数字
        int lastCode = Integer.parseInt(codeList.get(4));           // 倒数第一个数字
        // 双人玩法
        // -- 大/小
        if(lastCode <= 4) {
            sb.append("min");
        }else {
            sb.append("max");
        }
        sb.append(",");
        // -- 单/双
        if(lastCode % 2 == 0) {
            sb.append("even");
        }else {
            sb.append("odd");
        }
        sb.append(",");
        // 四人玩法
        if(lastSecondCode <= 4) {   // 小
            if(lastCode % 2 == 0) {
                sb.append("min_even");
            }else {
                sb.append("min_odd");
            }
        }else {     // 大
            if(lastCode % 2 == 0) {
                sb.append("max_even");
            }else {
                sb.append("max_odd");
            }
        }
        sb.append(",");
        // 十人
        sb.append(lastCode);
        return sb.toString();
    }


    private void handleWinningRecord() {
        // 查询未结算期数信息
        List<DbOpenRecord> unHandlePrizeList = duobaoMapper.selectUnhandlePrizeRecord();
        //TODO 此处需要使用多线程进行处理；否则会出现效率低下的问题
        for(DbOpenRecord unhandlePirze : unHandlePrizeList) {
            // 根据当期期数获取当期所有下注信息，进行中奖的验证及中奖结果更新
            int successCount = 0;	// 记录成功更新中奖结果的数目
            List<BetDetail> unhandleBetDetails = userMapper.selectBetDetailsByIssueNo(unhandlePirze.getIssueNo());
            for(BetDetail betDetail : unhandleBetDetails) {
                successCount += handlePrize(unhandlePirze.getOpenCode(), unhandlePirze.getOpenResult(), betDetail.getBetNum(),
                        betDetail.getAmount(), betDetail.getPerPrice(), betDetail.getPerWinPrice(), betDetail.getId(), betDetail.getUserId());
            }
            // 当成功更新中奖结果数，与待更新中奖记录数一致时，说明本期中奖结果已完全处理完毕；将本期中奖结算状态更新为已结算
            if(successCount == unhandleBetDetails.size()) {
                userMapper.updateOpenRecordFinished(unhandlePirze.getId());
            }
        }
    }

    /**
     * 对下注记录进行开奖处理
     * @return
     */
    @Transactional
    protected int handlePrize(String openCode, String openResult, String betNum, Integer betAmount,
                              BigDecimal perPrice, BigDecimal perWinPrice, Long betId, Long userId) {
        LogManager.getLogger(DuobaoService.class).info("openResult:"+openResult+"; betNum:"+betNum+"; betAmount:"+betAmount+
                "; perPrice:"+perPrice+"; perWinPrice:"+perWinPrice+"; betId:"+betId+"; userId:"+userId);
        boolean win = false;
        BigDecimal prizeMoney = new BigDecimal(betAmount).multiply(perWinPrice);

        List<String> resultList = Arrays.asList(openResult.split(","));
        if(resultList.contains(betNum)) {
            win = true;
        }

        if(!win) {
            prizeMoney = BigDecimal.ZERO;
        }

        if(userMapper.updateBetDetail(betId, openCode, win ? WinStatus.WIN.getCode() : WinStatus.LOSE.getCode(), prizeMoney) > 0) {
            if(win) {
                // 处理用户中奖资金
                userService.increaseBalance(userId, prizeMoney, SpendType.WIN.getCode());
            }
            return 1;
        }else {
            return 0;
        }
    }
}
